/*
 * Ext GWT 2.2.5 - Ext for GWT
 * Copyright(c) 2007-2010, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */
package com.extjs.gxt.ui.client.widget.grid;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
import com.extjs.gxt.ui.client.Style.SortDir;
import com.extjs.gxt.ui.client.core.DomHelper;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.data.BaseModel;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.PagingLoadConfig;
import com.extjs.gxt.ui.client.data.SortInfo;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.BaseObservable;
import com.extjs.gxt.ui.client.event.ColumnModelEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.EventType;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.js.JsArray;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.util.DelayedTask;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.util.Size;
import com.extjs.gxt.ui.client.util.Util;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ComponentHelper;
import com.extjs.gxt.ui.client.widget.menu.CheckMenuItem;
import com.extjs.gxt.ui.client.widget.menu.Menu;
import com.extjs.gxt.ui.client.widget.menu.MenuItem;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Widget;

/**
 * This class encapsulates the user interface of an {@link Grid}. Methods of
 * this class may be used to access user interface elements to enable special
 * display effects. Do not change the DOM structure of the user interface. </p>
 * <p />
 * This class does not provide ways to manipulate the underlying data. The data
 * model of a Grid is held in an {@link ListStore}.
 */
public class GridView extends BaseObservable
{

	/**
	 * Icons used by Grid which can be overridden as needed. s
	 */
	public class GridViewImages
	{
		private AbstractImagePrototype columns = AbstractImagePrototype
				.create(GXT.IMAGES.grid_columns());
		private AbstractImagePrototype sortAsc = AbstractImagePrototype
				.create(GXT.IMAGES.grid_sortAsc());
		private AbstractImagePrototype sortDesc = AbstractImagePrototype
				.create(GXT.IMAGES.grid_sortDesc());

		public AbstractImagePrototype getColumns()
		{
			return columns;
		}

		public AbstractImagePrototype getSortAsc()
		{
			return sortAsc;
		}

		public AbstractImagePrototype getSortDesc()
		{
			return sortDesc;
		}

		public void setColumns(AbstractImagePrototype columnsIcon)
		{
			this.columns = columnsIcon;
		}

		public void setSortAsc(AbstractImagePrototype sortAscIcon)
		{
			this.sortAsc = sortAscIcon;
		}

		public void setSortDesc(AbstractImagePrototype sortDescIcon)
		{
			this.sortDesc = sortDescIcon;
		}
	}

	private static JavaScriptObject colRe;
	protected int activeHdIndex;
	protected boolean autoFill;
	protected int borderWidth = 2;

	protected ColumnModel cm;
	protected Listener<ColumnModelEvent> columnListener;
	protected boolean deferEmptyText;
	protected ListStore<ModelData> ds;
	// elements
	protected El el, mainWrap, mainHd, innerHd, scroller, mainBody, focusEl;
	protected String emptyText = "&nbsp;";
	protected boolean enableHdMenu = true;
	// config
	protected boolean enableRowBody;
	protected boolean focusEnabled = true;
	protected ColumnFooter footer;
	protected boolean forceFit;

	protected Grid<ModelData> grid;
	protected ColumnHeader header;
	protected int headerColumnIndex;
	protected boolean headerDisabled;
	protected GridViewImages images;

	protected int lastViewWidth;
	protected StoreListener<ModelData> listener;

	protected Element overRow;
	protected boolean preventScrollToTopOnRefresh;
	protected int scrollOffset = XDOM.getScrollBarWidth();
	protected boolean selectable = false;
	protected SortInfo sortState;
	protected int splitterWidth = 5;
	protected GridTemplates templates;
	protected boolean userResized;
	// we first render grid with a vbar, and remove as needed
	protected boolean vbar = true;

	protected GridViewConfig viewConfig;
	protected List<List<Widget>> widgetList = new ArrayList<List<Widget>>();
	private DelayedTask addTask = new DelayedTask(new Listener<BaseEvent>()
	{
		@Override
		public void handleEvent(BaseEvent be)
		{
			calculateVBar(false);
			refreshFooterData();
		}
	});
	private boolean adjustForHScroll = true;
	private String cellSelector = "td.x-grid3-cell";
	private int cellSelectorDepth = 4;

	private DelayedTask removeTask = new DelayedTask(new Listener<BaseEvent>()
	{
		@Override
		public void handleEvent(BaseEvent be)
		{
			calculateVBar(false);
			applyEmptyText();
			refreshFooterData();
			processRows(0, false);
		}
	});
	private String rowSelector = "div.x-grid3-row";
	private int rowSelectorDepth = 10;
	private boolean showDirtyCells = true;

	private boolean showInvalidCells;

	/**
	 * Ensured the current row and column is visible.
	 * 
	 * @param row
	 *            the row index
	 * @param col
	 *            the column index
	 * @param hscroll
	 *            true to scroll horizontally if needed
	 * @return the calculated point
	 */
	public Point ensureVisible(int row, int col, boolean hscroll)
	{
		if (grid == null || !grid.isViewReady() || row < 0
				|| row > ds.getCount())
		{
			return null;
		}

		if (col == -1)
		{
			col = 0;
		}

		Element rowEl = getRow(row);
		Element cellEl = null;
		if (!(!hscroll && col == 0))
		{
			while (cm.isHidden(col))
			{
				col++;
			}
			cellEl = getCell(row, col);

		}

		if (rowEl == null)
		{
			return null;
		}

		Element c = scroller.dom;

		int ctop = 0;
		Element p = rowEl, stope = el.dom;
		while (p != null && p != stope)
		{
			ctop += p.getOffsetTop();
			p = p.getOffsetParent().cast();
		}
		ctop -= mainHd.dom.getOffsetHeight();

		int cbot = ctop + rowEl.getOffsetHeight();

		int ch = c.getOffsetHeight();
		int stop = c.getScrollTop();
		int sbot = stop + ch;

		if (ctop < stop)
		{
			c.setScrollTop(ctop);
		} else if (cbot > sbot)
		{
			if (hscroll
					&& (cm.getTotalWidth() > scroller.getWidth() - scrollOffset))
			{
				cbot += scrollOffset;
			}
			c.setScrollTop(cbot -= ch);
		}

		if (hscroll && cellEl != null)
		{
			int cleft = cellEl.getOffsetLeft();
			int cright = cleft + cellEl.getOffsetWidth();
			int sleft = c.getScrollLeft();
			int sright = sleft + c.getOffsetWidth();
			if (cleft < sleft)
			{
				c.setScrollLeft(cleft);
			} else if (cright > sright)
			{
				c.setScrollLeft(cright - scroller.getStyleWidth());
			}
		}

		return cellEl != null ? fly(cellEl).getXY() : new Point(
				c.getScrollLeft(), fly(rowEl).getY());
	}

	/**
	 * Returns the cell.
	 * 
	 * @param elem
	 *            the cell element or a child element
	 * @return the cell element
	 */
	public Element findCell(Element elem)
	{
		if (elem == null)
		{
			return null;
		}
		return fly(elem).findParentElement(cellSelector, cellSelectorDepth);
	}

	/**
	 * Returns the cell index.
	 * 
	 * @param elem
	 *            the cell or child element
	 * @param requiredStyle
	 *            an optional required style name
	 * @return the cell index or -1 if not found
	 */
	public int findCellIndex(Element elem, String requiredStyle)
	{
		Element cell = findCell(elem);
		if (cell != null
				&& (requiredStyle == null || fly(cell).hasStyleName(
						requiredStyle)))
		{
			return getCellIndex(cell);
		}
		return -1;
	}

	/**
	 * Returns the row element.
	 * 
	 * @param el
	 *            the row element or any child element
	 * @return the matching row element
	 */
	public Element findRow(Element el)
	{
		if (el == null)
		{
			return null;
		}
		return fly(el).findParentElement(rowSelector, rowSelectorDepth);
	}

	/**
	 * Returns the row index.
	 * 
	 * @param elem
	 *            the row or child of the row element
	 * @return the index
	 */
	public int findRowIndex(Element elem)
	{
		Element r = findRow(elem);
		return r != null ? r.getPropertyInt("rowIndex") : -1;
	}

	/**
	 * Focus the cell and scrolls into view.
	 * 
	 * @param rowIndex
	 *            the row index
	 * @param colIndex
	 *            the column index
	 * @param hscroll
	 *            true to scroll horizontally
	 */
	public void focusCell(int rowIndex, int colIndex, boolean hscroll)
	{
		Point xy = ensureVisible(rowIndex, colIndex, hscroll);
		if (xy != null)
		{
			focusEl.setXY(xy);
			if (focusEnabled)
			{
				focusGrid();
			}
		}
	}

	/**
	 * Focus the row and scrolls into view.
	 * 
	 * @param rowIndex
	 *            the row index
	 */
	public void focusRow(int rowIndex)
	{
		focusCell(rowIndex, 0, true);
	}

	/**
	 * Returns the grid's body element.
	 * 
	 * @return the body element
	 */
	public El getBody()
	{
		return scroller;
	}

	/**
	 * Returns the grid's &lt;TD> HtmlElement at the specified coordinates.
	 * 
	 * @param row
	 *            the row index in which to find the cell
	 * @param col
	 *            the column index of the cell
	 * @return the &lt;TD> at the specified coordinates
	 */
	public Element getCell(int row, int col)
	{
		// ROW DIV TABLE TR TD
		Element rowEl = getRow(row);
		return (Element) ((rowEl != null && rowEl.hasChildNodes()) ? rowEl
				.getFirstChild().getFirstChild().getFirstChild()
				.getChildNodes().getItem(col) : null);
	}

	/**
	 * Returns the cell selector depth.
	 * 
	 * @return the cell selector depth
	 */
	public int getCellSelectorDepth()
	{
		return cellSelectorDepth;
	}

	/**
	 * Returns the editor parent element.
	 * 
	 * @return the editor element
	 */
	public Element getEditorParent()
	{
		return scroller.dom;
	}

	/**
	 * Returns the empty text.
	 * 
	 * @return the empty text
	 */
	public String getEmptyText()
	{
		return emptyText;
	}

	/**
	 * Returns the grid's column header.
	 * 
	 * @return the header
	 */
	public ColumnHeader getHeader()
	{
		return header;
	}

	/**
	 * Returns the &lt;TD> HtmlElement which represents the Grid's header cell
	 * for the specified column index.
	 * 
	 * @param index
	 *            the column index
	 * @return the &lt;TD> element.
	 */
	public Element getHeaderCell(int index)
	{
		return mainHd.dom.getElementsByTagName("td").getItem(index);
	}

	/**
	 * Returns the images used by grid.
	 * 
	 * @return the images
	 */
	public GridViewImages getImages()
	{
		if (images == null)
		{
			images = new GridViewImages();
		}
		return images;
	}

	/**
	 * Return the &lt;TR> HtmlElement which represents a Grid row for the
	 * specified index.
	 * 
	 * @param row
	 *            the row index
	 * @return the &lt;TR> element
	 */
	public Element getRow(int row)
	{
		if (row < 0)
		{
			return null;
		}
		return getRows().getItem(row);
	}

	/**
	 * Return the &lt;TR> HtmlElement which represents a Grid row for the
	 * specified model.
	 * 
	 * @param m
	 *            the model
	 * @return the &lt;TR> element
	 */
	public Element getRow(ModelData m)
	{
		return getRow(ds.indexOf(m));
	}

	/**
	 * Returns the row selector depth.
	 * 
	 * @return the row selector depth
	 */
	public int getRowSelectorDepth()
	{
		return rowSelectorDepth;
	}

	/**
	 * Returns the scroll element.
	 * 
	 * @return the scroll element
	 */
	public El getScroller()
	{
		return scroller;
	}

	/**
	 * Returns the current scroll state.
	 * 
	 * @return the scroll state
	 */
	public Point getScrollState()
	{
		return new Point(scroller.getScrollLeft(), scroller.getScrollTop());
	}

	/**
	 * Returns the view config.
	 * 
	 * @return the view config
	 */
	public GridViewConfig getViewConfig()
	{
		return viewConfig;
	}

	/**
	 * Returns the widget at the current location.
	 * 
	 * @param rowIndex
	 *            the row index
	 * @param colIndex
	 *            the column index
	 * @return the widget or null
	 */
	public Widget getWidget(int rowIndex, int colIndex)
	{
		List<Widget> map = rowIndex < widgetList.size() ? widgetList
				.get(rowIndex) : null;
		return map != null && colIndex < map.size() ? map.get(colIndex) : null;
	}

	/**
	 * Returns true if the grid width will be adjusted based on visibility of
	 * horizontal scroll bar.
	 * 
	 * @return true if adjusting
	 */
	public boolean isAdjustForHScroll()
	{
		return adjustForHScroll;
	}

	/**
	 * Returns true if auto fill is enabled.
	 * 
	 * @return true for auto fill
	 */
	public boolean isAutoFill()
	{
		return autoFill;
	}

	/**
	 * Returns true if force fit is enabled.
	 * 
	 * @return true for force fit
	 */
	public boolean isForceFit()
	{
		return forceFit;
	}

	/**
	 * Returns true if dirty cell markers are enabled.
	 * 
	 * @return true of dirty cell markers
	 */
	public boolean isShowDirtyCells()
	{
		return showDirtyCells;
	}

	/**
	 * Returns true if invalid cell markers are enabled.
	 * 
	 * @return true if enabled
	 */
	public boolean isShowInvalidCells()
	{
		return showInvalidCells;
	}

	/**
	 * Returns true if sorting is enabled.
	 * 
	 * @return true for sorting
	 */
	public boolean isSortingEnabled()
	{
		return !headerDisabled;
	}

	public void layout()
	{
		layout(false);
	}

	/**
	 * Rebuilds the grid using its current configuration and data.
	 * 
	 * @param headerToo
	 *            true to refresh the header
	 */
	public void refresh(boolean headerToo)
	{
		if (grid != null && grid.isViewReady())
		{
			stopEditing();

			detachWidgets(0, -1, true);

			if (!preventScrollToTopOnRefresh)
			{
				scrollToTop();
			}
			mainBody.setInnerHtml(renderRows(0, -1));
			if (headerToo)
			{
				sortState = null;
				header.release();

				newColumnHeader();
				renderHeader();
				if (grid.isAttached())
				{
					ComponentHelper.doAttach(header);
				}
				header.setEnableColumnResizing(grid.isColumnResize());
				header.setEnableColumnReorder(grid.isColumnReordering());
			}
			processRows(0, true);
			renderWidgets(0, -1);

			if (footer != null)
			{
				ComponentHelper.doDetach(footer);
				footer.el().removeFromParent();
			}
			if (cm.getAggregationRows().size() > 0)
			{
				footer = new ColumnFooter(grid, cm);
				renderFooter();
				if (grid.isAttached())
				{
					ComponentHelper.doAttach(footer);
				}
			}

			calculateVBar(true);

			updateHeaderSortState();

			applyEmptyText();
			constrainFocusElement();

			fireEvent(Events.Refresh);
		}
	}

	/**
	 * Scrolls the grid to the top.
	 */
	public void scrollToTop()
	{
		scroller.setScrollTop(0);
		scroller.setScrollLeft(0);
	}

	/**
	 * True to adjust the grid width when the horizontal scrollbar is hidden and
	 * visible (defaults to true).
	 * 
	 * @param adjustForHScroll
	 *            true to adjust for horizontal scroll bar
	 */
	public void setAdjustForHScroll(boolean adjustForHScroll)
	{
		this.adjustForHScroll = adjustForHScroll;
	}

	/**
	 * True to auto expand the columns to fit the grid <b>when the grid is
	 * created</b>.
	 * 
	 * @param autoFill
	 *            true to expand
	 */
	public void setAutoFill(boolean autoFill)
	{
		this.autoFill = autoFill;
	}

	/**
	 * The number of levels to search for cells in event delegation (defaults to
	 * 4).
	 * 
	 * @param cellSelectorDepth
	 *            the cell selector depth
	 */
	public void setCellSelectorDepth(int cellSelectorDepth)
	{
		this.cellSelectorDepth = cellSelectorDepth;
	}

	/**
	 * Default text to display in the grid body when no rows are available
	 * (defaults to '').
	 * 
	 * @param emptyText
	 *            the empty text
	 */
	public void setEmptyText(String emptyText)
	{
		this.emptyText = emptyText;
	}

	/**
	 * True to auto expand/contract the size of the columns to fit the grid
	 * width and prevent horizontal scrolling.
	 * 
	 * @param forceFit
	 *            true to force fit
	 */
	public void setForceFit(boolean forceFit)
	{
		this.forceFit = forceFit;
	}

	/**
	 * The number of levels to search for rows in event delegation (defaults to
	 * 10).
	 * 
	 * @param rowSelectorDepth
	 *            the row selector depth
	 */
	public void setRowSelectorDepth(int rowSelectorDepth)
	{
		this.rowSelectorDepth = rowSelectorDepth;
	}

	/**
	 * True to display a red triangle in the upper left corner of any cells
	 * which are "dirty" as defined by any existing records in the data store
	 * (defaults to true).
	 * 
	 * @param showDirtyCells
	 *            true to display the dirty flag
	 */
	public void setShowDirtyCells(boolean showDirtyCells)
	{
		this.showDirtyCells = showDirtyCells;
	}

	/**
	 * True to enabled invalid cell markers (defaults to false).
	 * 
	 * @param showInvalidCells
	 *            true to enable
	 */
	public void setShowInvalidCells(boolean showInvalidCells)
	{
		this.showInvalidCells = showInvalidCells;
	}

	/**
	 * True to allow column sorting when the user clicks a column (defaults to
	 * true).
	 * 
	 * @param sortable
	 *            true for sortable columns
	 */
	public void setSortingEnabled(boolean sortable)
	{
		this.headerDisabled = !sortable;
	}

	/**
	 * Sets the view config.
	 * 
	 * @param viewConfig
	 *            the view config
	 */
	public void setViewConfig(GridViewConfig viewConfig)
	{
		this.viewConfig = viewConfig;
	}

	protected void addRowStyle(Element elem, String style)
	{
		if (elem != null)
		{
			fly(elem).addStyleName(style);
		}
	}

	protected void afterRender()
	{

		mainBody.setInnerHtml(renderRows(0, -1));
		renderWidgets(0, -1);
		processRows(0, true);

		if (footer != null && grid.getLazyRowRender() > 0)
		{
			footer.refresh();
		}

		int sh = scroller.getHeight();
		int dh = mainBody.getHeight();
		boolean vbar = dh < sh;
		if (vbar)
		{
			this.vbar = !vbar;
			lastViewWidth = -1;
			layout();
		}

		applyEmptyText();

	}

	protected void applyEmptyText()
	{
		if (emptyText == null)
		{
			emptyText = "&nbsp;";
		}
		if (!hasRows())
		{
			mainBody.setInnerHtml("<div class='x-grid-empty'>" + emptyText
					+ "</div>");
		}
		syncHScroll();
	}

	protected void autoExpand(boolean preventUpdate)
	{
		if (!userResized && grid.getAutoExpandColumn() != null)
		{
			int tw = cm.getTotalWidth(false);
			int aw = grid.getWidth(true) - getScrollAdjust();
			if (tw != aw)
			{
				int ci = cm.getIndexById(grid.getAutoExpandColumn());
				assert ci != Style.DEFAULT : "auto expand column not found";
				if (cm.isHidden(ci))
				{
					return;
				}
				int currentWidth = cm.getColumnWidth(ci);
				int cw = Math.min(
						Math.max(((aw - tw) + currentWidth),
								grid.getAutoExpandMin()),
						grid.getAutoExpandMax());
				if (cw != currentWidth)
				{
					cm.setColumnWidth(ci, cw, true);

					if (!preventUpdate)
					{
						updateColumnWidth(ci, cw);
					}
				}
			}
		}
	}

	protected void calculateVBar(boolean force)
	{
		if (force)
		{
			resize();
		}
		int sh = scroller.getHeight();
		int dh = mainBody.getHeight();
		boolean vbar = dh > sh;
		if (force || this.vbar != vbar)
		{
			this.vbar = vbar;
			lastViewWidth = -1;
			layout(true);
		}
	}

	@SuppressWarnings(
	{ "rawtypes", "unchecked" })
	protected GridEvent<?> createComponentEvent(Event event)
	{
		return new GridEvent(grid, event);
	}

	protected Menu createContextMenu(final int colIndex)
	{
		final Menu menu = new Menu();

		if (cm.isSortable(colIndex))
		{
			MenuItem item = new MenuItem();
			item.setText(GXT.MESSAGES.gridView_sortAscText());
			item.setIcon(getImages().getSortAsc());
			item.addSelectionListener(new SelectionListener<MenuEvent>()
			{
				@Override
				public void componentSelected(MenuEvent ce)
				{
					doSort(colIndex, SortDir.ASC);
				}

			});
			menu.add(item);

			item = new MenuItem();
			item.setText(GXT.MESSAGES.gridView_sortDescText());
			item.setIcon(getImages().getSortDesc());
			item.addSelectionListener(new SelectionListener<MenuEvent>()
			{
				@Override
				public void componentSelected(MenuEvent ce)
				{
					doSort(colIndex, SortDir.DESC);
				}
			});
			menu.add(item);
		}

		MenuItem columns = new MenuItem();
		columns.setText(GXT.MESSAGES.gridView_columnsText());
		columns.setIcon(getImages().getColumns());
		columns.setData("gxt-columns", "true");

		final Menu columnMenu = new Menu();

		int cols = cm.getColumnCount();
		for (int i = 0; i < cols; i++)
		{
			if (shouldNotCount(i, false))
			{
				continue;
			}
			final int fcol = i;
			final CheckMenuItem check = new CheckMenuItem();
			check.setHideOnClick(false);
			check.setText(cm.getColumnHeader(i));
			check.setChecked(!cm.isHidden(i));
			check.addSelectionListener(new SelectionListener<MenuEvent>()
			{
				@Override
				public void componentSelected(MenuEvent ce)
				{
					cm.setHidden(fcol, !cm.isHidden(fcol));
					restrictMenu(columnMenu);
				}
			});
			columnMenu.add(check);
		}

		restrictMenu(columnMenu);
		columns.setEnabled(columnMenu.getItemCount() > 0);
		columns.setSubMenu(columnMenu);
		menu.add(columns);
		return menu;
	}

	protected void detachWidget(int rowIndex, boolean remove)
	{
		List<Widget> m = rowIndex < widgetList.size() ? widgetList
				.get(rowIndex) : null;
		if (m != null)
		{
			for (Widget w : m)
			{
				ComponentHelper.doDetach(w);
				if (w != null)
				{
					El.fly(w.getElement()).removeFromParent();
				}
			}
			if (remove)
			{
				widgetList.remove(rowIndex);
			}
		}
	}

	protected void detachWidgets(int startRow, int endRow, boolean remove)
	{
		if (endRow == -1)
		{
			endRow = widgetList.size() - 1;
		}
		for (int i = endRow; i >= startRow; i--)
		{
			detachWidget(i, remove);
		}

	}

	protected void doAttach()
	{
		ComponentHelper.doAttach(header);
		ComponentHelper.doAttach(footer);
		resize();
		renderWidgets(0, -1);
	}

	protected void doDetach()
	{
		ComponentHelper.doDetach(header);
		ComponentHelper.doDetach(footer);
		detachWidgets(0, -1, false);
	}

	protected String doRender(List<ColumnData> cs, List<ModelData> rows,
			int startRow, int colCount, boolean stripe)
	{
		int last = colCount - 1;
		String tstyle = "width:" + getTotalWidth() + "px;";

		StringBuilder buf = new StringBuilder();

		for (int j = 0; j < rows.size(); j++)
		{
			ModelData model = (ModelData) rows.get(j);

			model = prepareData(model);

			Record r = ds.hasRecord(model) ? ds.getRecord(model) : null;

			int rowBodyColSpanCount = colCount;
			if (enableRowBody)
			{
				if (grid.getSelectionModel() instanceof CheckBoxSelectionModel<?>)
				{
					CheckBoxSelectionModel<?> sm = (CheckBoxSelectionModel<?>) grid
							.getSelectionModel();
					if (cm.getColumnById(sm.getColumn().getId()) != null)
					{
						rowBodyColSpanCount--;
					}
				}
				for (ColumnConfig c : cm.getColumns())
				{
					if (c instanceof RowExpander || c instanceof RowNumberer)
					{
						rowBodyColSpanCount--;
					}
				}
			}
			int rowIndex = (j + startRow);

			if (GXT.isAriaEnabled())
			{
				buf.append("<div role=\"row\" aria-level=\"2\" class=\"x-grid3-row ");
			} else
			{
				buf.append("<div class=\"x-grid3-row ");
			}

			if (stripe && ((rowIndex + 1) % 2 == 0))
			{
				buf.append(" x-grid3-row-alt");
			}
			if (!selectable)
			{
				buf.append(" x-unselectable-single");
			}

			if (showDirtyCells && r != null && r.isDirty())
			{
				buf.append(" x-grid3-dirty-row");
			}
			if (viewConfig != null)
			{
				buf.append(" ");
				buf.append(viewConfig.getRowStyle(model, rowIndex, ds));
			}
			buf.append("\" style=\"");
			buf.append(tstyle);
			buf.append("\" id=\"");
			buf.append(grid.getId());
			buf.append("_");
			buf.append(ds.getKeyProvider() != null ? ds.getKeyProvider()
					.getKey(model) : XDOM.getUniqueId());
			buf.append("\" unselectable=\"");
			buf.append(selectable ? "off" : "on");
			buf.append("\"><table class=x-grid3-row-table role=presentation border=0 cellspacing=0 cellpadding=0 style=\"");

			buf.append(tstyle);
			buf.append("\"><tbody role=presentation><tr role=presentation>");
			widgetList.add(rowIndex, new ArrayList<Widget>());
			for (int i = 0; i < colCount; i++)
			{
				ColumnData c = cs.get(i);
				c.css = c.css == null ? "" : c.css;
				String rv = getRenderedValue(c, rowIndex, i, model, c.name);
				String role = "gridcell";
				if (GXT.isAriaEnabled())
				{
					ColumnConfig cc = cm.getColumn(i);
					if (cc.isRowHeader())
					{
						role = "rowheader";
					}
				}

				String attr = c.cellAttr != null ? c.cellAttr : "";
				String cellAttr = c.cellAttr != null ? c.cellAttr : "";

				buf.append("<td id=\"" + XDOM.getUniqueId() + "\" role=\""
						+ role
						+ "\" class=\"x-grid3-col x-grid3-cell x-grid3-td-");
				buf.append(c.id);
				buf.append(" ");
				buf.append(i == 0 ? "x-grid-cell-first "
						: (i == last ? "x-grid3-cell-last " : ""));
				if (c.css != null)
				{
					buf.append(c.css);
				}
				if (showInvalidCells && r != null && !r.isValid(c.name))
				{
					buf.append(" x-grid3-invalid-cell");
				}
				if (showDirtyCells && r != null
						&& r.getChanges().containsKey(c.name))
				{
					buf.append(" x-grid3-dirty-cell");
				}

				buf.append("\" style=\"");
				buf.append(c.style);
				buf.append("\" ");
				buf.append(cellAttr);
				buf.append("><div unselectable=\"");
				buf.append(selectable ? "off" : "on");
				buf.append("\" class=\"x-grid3-cell-inner x-grid3-col-");
				buf.append(c.id);
				buf.append("\" ");
				buf.append(attr);
				buf.append(">");
				buf.append(rv);
				buf.append("</div></td>");
			}

			buf.append("</tr>");
			if (enableRowBody)
			{
				buf.append("<tr class=x-grid3-row-body-tr style=\"\"><td colspan=");
				buf.append(rowBodyColSpanCount);
				buf.append(" class=x-grid3-body-cell><div class=x-grid3-row-body>${body}</div></td></tr>");
			}
			buf.append("</tbody></table></div>");
		}

		return buf.toString();
	}

	protected void doSort(int colIndex, SortDir sortDir)
	{
		ds.sort(cm.getDataIndex(colIndex), sortDir);
	}

	protected void fitColumns(boolean preventRefresh, boolean onlyExpand,
			int omitColumn)
	{
		int tw = cm.getTotalWidth(false);
		double aw = grid.el().getWidth(true) - getScrollAdjust();
		if (aw <= 0)
		{
			aw = grid.el().getStyleWidth();
		}

		if (aw < 20 || aw > 2000)
		{ // not initialized, so don't screw up the
			// default widths
			return;
		}

		int extra = (int) aw - tw;

		if (extra == 0)
		{
			return;
		}

		int vc = cm.getColumnCount(true);
		int ac = vc - (omitColumn != -1 ? 1 : 0);

		if (ac == 0)
		{
			ac = 1;
			omitColumn = -1;
		}

		int colCount = cm.getColumnCount();
		Stack<Integer> cols = new Stack<Integer>();
		int width = 0;
		int w;

		for (int i = 0; i < colCount; i++)
		{
			if (!cm.isHidden(i) && !cm.isFixed(i) && i != omitColumn)
			{
				w = cm.getColumnWidth(i);
				cols.push(i);
				cols.push(w);
				width += w;
			}
		}

		double frac = (aw - cm.getTotalWidth()) / width;
		while (cols.size() > 0)
		{
			w = cols.pop();
			int i = cols.pop();
			int ww = Math.max(grid.getMinColumnWidth(),
					(int) Math.floor(w + w * frac));
			cm.setColumnWidth(i, ww, true);
		}

		if (!preventRefresh)
		{
			updateAllColumnWidths();
		}
	}

	protected El fly(Element elem)
	{
		return El.fly(elem, "grid");
	}

	protected void focusGrid()
	{
		if (GXT.isIE)
		{
			Scheduler.get().scheduleDeferred(new ScheduledCommand()
			{
				@Override
				public void execute()
				{
					if (LocaleInfo.getCurrentLocale().isRTL())
					{
						// special fix for IE in RTL the change applied on the
						// fliweight dom causes a reset on the scrollTop,
						// scrollLeft
						// so we need to reset them to their values before the
						// focus occured.
						int topBeforeFocus = scroller.getScrollTop();
						int leftBeforeFocus = scroller.getScrollLeft();
						focusEl.setFocus(true);
						if (scroller.getScrollTop() != topBeforeFocus)
							scroller.setScrollTop(topBeforeFocus);
						if (scroller.getScrollLeft() != leftBeforeFocus)
							scroller.setScrollLeft(leftBeforeFocus);
					} else
					{
						focusEl.setFocus(true);
					}
				}
			});
		} else
		{
			focusEl.setFocus(true);
		}
	}

	protected int getCellIndex(Element elem)
	{
		if (elem != null)
		{
			String id = getCellIndexId(elem);
			if (id != null)
			{
				return cm.getIndexById(id);
			}
		}
		return -1;
	}

	protected List<ColumnData> getColumnData()
	{
		int colCount = cm.getColumnCount();
		List<ColumnData> cs = new ArrayList<ColumnData>();
		for (int i = 0; i < colCount; i++)
		{
			String name = cm.getDataIndex(i);
			ColumnData data = new ColumnData();
			data.name = name == null ? cm.getColumnId(i) : name;
			data.renderer = cm.getRenderer(i);
			data.id = cm.getColumnId(i);
			data.style = getColumnStyle(i, false);
			cs.add(data);
		}
		return cs;
	}

	protected String getColumnStyle(int colIndex, boolean isHeader)
	{
		String style = !isHeader ? cm.getColumnStyle(colIndex) : "";
		if (style == null)
			style = "";
		int adj = GXT.isWebKit ? 2 : 0;
		style += "width:" + (getColumnWidth(colIndex) + adj) + "px;";
		if (cm.isHidden(colIndex))
		{
			style += "display:none;";
		}
		HorizontalAlignment align = cm.getColumnAlignment(colIndex);
		if (align != null)
		{
			align = Style.convertHorizontalAlignmentToStrict(align);
			style += "text-align:" + align.name().toLowerCase() + ";";
		}
		return style;
	}

	protected int getColumnWidth(int col)
	{
		int w = cm.getColumnWidth(col);
		return (GXT.isBorderBox ? w : (w - borderWidth > 0 ? w - borderWidth
				: 0));
	}

	protected int getOffsetWidth()
	{
		return (getTotalWidth() + getScrollAdjust());
	}

	protected String getRenderedValue(ColumnData data, int rowIndex,
			int colIndex, ModelData m, String property)
	{
		GridCellRenderer<ModelData> r = cm.getRenderer(colIndex);
		List<Widget> rowMap = widgetList.get(rowIndex);
		rowMap.add(colIndex, null);
		if (r != null)
		{
			Object o = r.render(ds.getAt(rowIndex), property, data, rowIndex,
					colIndex, ds, grid);
			if (o instanceof Widget)
			{
				Widget w = (Widget) o;

				rowMap.set(colIndex, w);
				return "";
			} else if (o != null)
			{
				return o.toString();
			}
		}
		Object val = m.get(property);

		ColumnConfig c = cm.getColumn(colIndex);

		if (val != null && val instanceof Number && c.getNumberFormat() != null)
		{
			Number n = (Number) val;
			val = c.getNumberFormat().format(n.doubleValue());
		} else if (val != null && val instanceof Date
				&& c.getDateTimeFormat() != null)
		{
			DateTimeFormat dtf = c.getDateTimeFormat();
			val = dtf.format((Date) val);
		}

		String text = null;
		if (val != null)
		{
			text = val.toString();
		}
		return Util.isEmptyString(text) ? "&#160;" : text;
	}

	protected NodeList<Element> getRows()
	{
		if (!hasRows())
		{
			return new JsArray().getJsObject().cast();
		}
		return mainBody.dom.getChildNodes().cast();
	}

	protected int getScrollAdjust()
	{
		return adjustForHScroll ? (scroller != null ? (vbar ? scrollOffset : 2)
				: scrollOffset) : scrollOffset;
	}

	protected SortInfo getSortState()
	{
		return ds.getSortState();
	}

	protected int getTotalWidth()
	{
		return cm.getTotalWidth();
	}

	protected Element getWidgetCell(int row, int cell)
	{
		Element cellEl = getCell(row, cell);
		if (cellEl != null)
		{
			return cellEl.getFirstChildElement();
		}
		return null;
	}

	@SuppressWarnings(
	{ "unchecked", "rawtypes" })
	protected void handleComponentEvent(GridEvent ge)
	{
		switch (ge.getEventTypeInt())
		{
			case Event.ONMOUSEMOVE:
				Element row = getRow(ge.getRowIndex());
				if (overRow != null && row == null)
				{
					onRowOut(overRow);
				} else if (row != null && overRow != row)
				{
					if (overRow != null)
					{
						onRowOut(overRow);
					}
					onRowOver(row);
				}
			break;

			case Event.ONMOUSEOVER:
				EventTarget from = ge.getEvent().getRelatedEventTarget();
				if (from == null
						|| (Element.is(from) && !DOM.isOrHasChild(grid
								.getElement(),
								(com.google.gwt.user.client.Element) Element
										.as(from))))
				{
					Element r = getRow(ge.getRowIndex());
					if (r != null)
					{
						onRowOver(r);
					}
				}
			break;
			case Event.ONMOUSEOUT:
				EventTarget to = ge.getEvent().getRelatedEventTarget();
				if (to == null
						|| (Element.is(to) && !DOM.isOrHasChild(grid
								.getElement(),
								(com.google.gwt.user.client.Element) Element
										.as(to))))
				{
					if (overRow != null)
					{
						onRowOut(overRow);
					}
				}
			break;
			case Event.ONMOUSEDOWN:
				onMouseDown(ge);
			break;
			case Event.ONSCROLL:
				if (scroller.isOrHasChild(ge.getTarget()))
				{
					syncScroll();
				}
			break;
		}
	}

	protected boolean hasRows()
	{
		if (mainBody == null)
		{
			return false;
		}
		Element e = mainBody.dom.getFirstChildElement();
		return e != null && !"x-grid-empty".equals(e.getClassName());
	}

	/**
	 * Initializes the view.
	 * 
	 * @param grid
	 *            the grid
	 */
	@SuppressWarnings(
	{ "unchecked", "rawtypes" })
	protected void init(final Grid grid)
	{
		this.grid = grid;
		this.cm = grid.getColumnModel();
		selectable = !grid.isDisableTextSelection();

		initListeners();

		initTemplates();
		initData(grid.getStore(), cm);
		initUI(grid);

		newColumnHeader();

		if (cm.getAggregationRows().size() > 0)
		{
			footer = new ColumnFooter(grid, cm);
		}
	}

	/**
	 * Initializes the data.
	 * 
	 * @param ds
	 *            the data store
	 * @param cm
	 *            the column model
	 */
	@SuppressWarnings(
	{ "unchecked", "rawtypes" })
	protected void initData(ListStore ds, ColumnModel cm)
	{
		if (this.ds != null)
		{
			this.ds.removeStoreListener(listener);
		}
		if (ds != null)
		{
			ds.addStoreListener(listener);
		}
		this.ds = ds;

		if (this.cm != null)
		{
			this.cm.removeListener(Events.HiddenChange, columnListener);
			this.cm.removeListener(Events.HeaderChange, columnListener);
			this.cm.removeListener(Events.WidthChange, columnListener);
			this.cm.removeListener(Events.ColumnMove, columnListener);
		}
		if (cm != null)
		{
			cm.addListener(Events.HiddenChange, columnListener);
			cm.addListener(Events.HeaderChange, columnListener);
			cm.addListener(Events.WidthChange, columnListener);
			cm.addListener(Events.ColumnMove, columnListener);
		}
		this.cm = cm;
	}

	protected void initElements()
	{
		NodeList<Node> cs = grid.getElement().getFirstChild().getChildNodes();

		el = grid.el().firstChild();

		mainWrap = new El((com.google.gwt.user.client.Element) cs.getItem(0));
		mainHd = mainWrap.firstChild();

		if (grid.isHideHeaders())
		{
			mainHd.setVisible(false);
		}

		innerHd = mainHd.firstChild();

		scroller = mainWrap.getChild(1);
		scroller.addEventsSunk(Event.ONSCROLL);

		if (forceFit)
		{
			scroller.setStyleAttribute("overflowX", "hidden");
		}

		mainBody = scroller.firstChild();
		focusEl = scroller.getChild(1);
		grid.swallowEvent(Events.OnClick, focusEl.dom, true);
	}

	protected void initListeners()
	{
		listener = new StoreListener<ModelData>()
		{

			@Override
			public void storeAdd(StoreEvent<ModelData> se)
			{
				onAdd(ds, se.getModels(), se.getIndex());
			}

			@Override
			public void storeBeforeDataChanged(StoreEvent<ModelData> se)
			{
				onBeforeDataChanged(se);
			}

			@Override
			public void storeClear(StoreEvent<ModelData> se)
			{
				onClear(se);
			}

			@Override
			public void storeDataChanged(StoreEvent<ModelData> se)
			{
				onDataChanged(se);
			}

			@Override
			public void storeFilter(StoreEvent<ModelData> se)
			{
				onDataChanged(se);
			}

			@Override
			public void storeRemove(StoreEvent<ModelData> se)
			{
				onRemove(ds, se.getModel(), se.getIndex(), false);
			}

			@Override
			public void storeUpdate(StoreEvent<ModelData> se)
			{
				onUpdate(ds, se.getModel());
			}

		};

		columnListener = new Listener<ColumnModelEvent>()
		{
			@Override
			public void handleEvent(ColumnModelEvent e)
			{
				if (grid.isViewReady())
				{
					EventType type = e.getType();
					if (type == Events.HiddenChange)
					{
						onHiddenChange(cm, e.getColIndex(), e.isHidden());
					} else if (type == Events.HeaderChange)
					{
						onHeaderChange(e.getColIndex(), e.getHeader());
					} else if (type == Events.WidthChange)
					{
						onColumnWidthChange(e.getColIndex(), e.getWidth());
					} else if (type == Events.ColumnMove)
					{
						onColumnMove(e.getColIndex());
					}
				}
			}
		};
	}

	protected void initTemplates()
	{
		templates = GWT.create(GridTemplates.class);
	}

	protected void initUI(final Grid<ModelData> grid)
	{

	}

	protected void insertRows(ListStore<ModelData> store, int firstRow,
			int lastRow, boolean isUpdate)
	{
		Element e = mainBody.dom.getFirstChildElement();
		if (e != null && !hasRows())
		{
			mainBody.dom.setInnerHTML("");
		}

		String html = renderRows(firstRow, lastRow);
		Element before = getRow(firstRow);

		if (before != null)
		{
			DomHelper.insertBefore((com.google.gwt.user.client.Element) before,
					html);
		} else
		{
			DomHelper.insertHtml("beforeEnd", mainBody.dom, html);
		}

		if (!isUpdate)
		{
			processRows(firstRow, false);
		}
	}

	protected void layout(boolean skipResize)
	{
		if (mainBody == null)
		{
			return;
		}

		El c = grid.el();
		Size csize = c.getStyleSize();

		int vw = csize.width;
		int vh = 0;
		if (vw < 10 || csize.height < 20)
		{
			return;
		}

		if (!skipResize)
		{
			resize();
		}

		if (forceFit || autoFill)
		{
			if (lastViewWidth != vw)
			{
				fitColumns(false, false, -1);
				header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
				if (footer != null)
				{
					footer.updateTotalWidth(getOffsetWidth(), getTotalWidth());
				}
				lastViewWidth = vw;
			}
		} else
		{
			autoExpand(false);
			header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
			if (footer != null)
			{
				footer.updateTotalWidth(getOffsetWidth(), getTotalWidth());
			}
			syncHeaderScroll();
		}

		templateOnLayout(vw, vh);
	}

	protected ColumnHeader newColumnHeader()
	{
		header = new ColumnHeader(grid, cm)
		{
			@SuppressWarnings("unchecked")
			@Override
			protected ComponentEvent createColumnEvent(ColumnHeader header,
					int column, Menu menu)
			{
				GridEvent<ModelData> event = (GridEvent<ModelData>) GridView.this
						.createComponentEvent(null);
				event.setColIndex(column);
				event.setMenu(menu);
				return event;
			}

			@Override
			protected Menu getContextMenu(int column)
			{
				return createContextMenu(column);
			}

			@Override
			protected void onColumnSplitterMoved(int colIndex, int width)
			{
				super.onColumnSplitterMoved(colIndex, width);
				GridView.this.onColumnSplitterMoved(colIndex, width);
			}

			@Override
			protected void onHeaderClick(ComponentEvent ce, int column)
			{
				super.onHeaderClick(ce, column);
				GridView.this.onHeaderClick(grid, column);
			}

			@Override
			protected void onKeyDown(ComponentEvent ce, int index)
			{
				ce.cancelBubble();
				if (grid.getSelectionModel() instanceof CellSelectionModel<?>)
				{
					CellSelectionModel<?> csm = (CellSelectionModel<?>) grid
							.getSelectionModel();
					csm.selectCell(0, index);
				} else
				{
					grid.getSelectionModel().select(0, false);
				}
			}

		};
		header.setSplitterWidth(splitterWidth);
		header.setMinColumnWidth(grid.getMinColumnWidth());

		return header;
	}

	protected void notifyHide()
	{
	}

	protected void notifyShow()
	{
	}

	protected void onAdd(ListStore<ModelData> store, List<ModelData> models,
			int index)
	{
		if (grid != null && grid.isViewReady())
		{
			insertRows(store, index, index + (models.size() - 1), false);
			renderWidgets(index, index + (models.size() - 1));
			addTask.delay(10);
		}
	}

	protected void onBeforeDataChanged(StoreEvent<ModelData> se)
	{
		if (grid != null && grid.isLoadMask())
		{
			grid.mask(GXT.MESSAGES.loadMask_msg());
		}
	}

	protected void onCellDeselect(int row, int col)
	{
		Element cell = getCell(row, col);
		if (cell != null)
		{
			fly(cell).removeStyleName("x-grid3-cell-selected");
			if (GXT.isAriaEnabled())
			{
				cell.setAttribute("aria-selected", "false");
			}
		}
	}

	protected void onCellSelect(int row, int col)
	{
		Element cell = getCell(row, col);
		if (cell != null)
		{
			fly(cell).addStyleName("x-grid3-cell-selected");
			if (GXT.isAriaEnabled())
			{
				cell.setAttribute("aria-selected", "true");
				grid.setAriaState("aria-activedescendant", cell.getId());
			}
		}
	}

	protected void onClear(StoreEvent<ModelData> se)
	{
		refresh(false);
	}

	protected void onClick(GridEvent<ModelData> ce)
	{
		Element row = findRow(ce.getTarget());
		if (row != null)
		{
			ce.setRowIndex(findRowIndex(row));
			grid.fireEvent(Events.RowClick, ce);
		}
	}

	protected void onColumnMove(int newIndex)
	{
		boolean pScroll = preventScrollToTopOnRefresh;
		preventScrollToTopOnRefresh = true;
		refresh(true);
		preventScrollToTopOnRefresh = pScroll;
		templateAfterMove(newIndex);
	}

	@SuppressWarnings("unchecked")
	protected void onColumnSplitterMoved(int colIndex, int width)
	{
		stopEditing();
		userResized = true;
		width = Math.max(grid.getMinColumnWidth(), width);
		cm.setColumnWidth(colIndex, width);

		GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
		e.setColIndex(colIndex);
		e.setWidth(width);
		grid.fireEvent(Events.ColumnResize, e);
	}

	protected void onColumnWidthChange(int column, int width)
	{
		if (forceFit)
		{
			fitColumns(false, false, column);
			header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
		} else
		{
			updateColumnWidth(column, width);
			header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
			if (GXT.isIE)
			{
				syncHeaderScroll();
			}
		}

		if (grid.isStateful())
		{
			Map<String, Object> state = grid.getState();
			state.put("width" + cm.getColumnId(column), width);
			grid.saveState();
		}
	}

	protected void onDataChanged(StoreEvent<ModelData> se)
	{
		refresh(false);
		if (grid != null && grid.isLoadMask())
		{
			if (grid.isEnabled())
			{
				grid.unmask();
			} else
			{
				grid.mask();
			}
		}

		if (grid != null && grid.isStateful() && ds != null
				&& ds.getLoadConfig() != null
				&& ds.getLoadConfig() instanceof PagingLoadConfig)
		{
			PagingLoadConfig config = (PagingLoadConfig) ds.getLoadConfig();
			Map<String, Object> state = grid.getState();
			state.put("offset", config.getOffset());
			state.put("limit", config.getLimit());
			grid.saveState();
		}
	}

	protected void onHeaderChange(int column, String text)
	{
		header.setHeader(column, text);
	}

	protected void onHeaderClick(Grid<ModelData> grid, int column)
	{
		this.headerColumnIndex = column;
		if (!headerDisabled && cm.isSortable(column))
		{
			doSort(column, null);
		}
	}

	protected void onHiddenChange(ColumnModel cm, int col, boolean hidden)
	{
		updateColumnHidden(col, hidden);

		if (grid.isStateful())
		{
			Map<String, Object> state = grid.getState();
			state.put("hidden" + cm.getColumnId(col), hidden);
			grid.saveState();
		}
	}

	protected void onHighlightRow(int rowIndex, boolean highlight)
	{
		Element row = getRow(rowIndex);
		if (row != null)
		{
			if (highlight)
			{
				addRowStyle(row, "x-grid3-highlightrow");
				if (GXT.isAriaEnabled())
				{
					grid.setAriaState("aria-activedescendant", row.getId());
				}
			} else
			{
				removeRowStyle(row, "x-grid3-highlightrow");
			}
		}
	}

	protected void onMouseDown(GridEvent<ModelData> ge)
	{

	}

	protected void onRemove(ListStore<ModelData> ds, ModelData m, int index,
			boolean isUpdate)
	{
		if (grid != null && grid.isViewReady())
		{
			detachWidget(index, true);
			removeRow(index);
			if (!isUpdate)
			{
				removeTask.delay(10);
			} else
			{
				removeTask.delay(0);
			}
			constrainFocusElement();
		}
	}

	protected void onRowDeselect(int rowIndex)
	{
		Element row = getRow(rowIndex);
		if (row != null)
		{
			removeRowStyle(row, "x-grid3-row-selected");
			removeRowStyle(row, "x-grid3-highlightrow ");
			if (GXT.isAriaEnabled())
			{
				row.setAttribute("aria-selected", "false");
			}
		}
	}

	protected void onRowOut(Element row)
	{
		if (grid.isTrackMouseOver())
		{
			removeRowStyle(row, "x-grid3-row-over");
			overRow = null;
		}
	}

	protected void onRowOver(Element row)
	{
		if (grid.isTrackMouseOver())
		{
			addRowStyle(row, "x-grid3-row-over");
			overRow = row;
		}
	}

	protected void onRowSelect(int rowIndex)
	{
		Element row = getRow(rowIndex);
		if (row != null)
		{
			onRowOut(row);
			addRowStyle(row, "x-grid3-row-selected");
			if (GXT.isAriaEnabled())
			{
				row.setAttribute("aria-selected", "true");
				grid.getElement().setAttribute("aria-activedescendant",
						row.getId());
			}
		}
	}

	protected void onUpdate(ListStore<ModelData> store, ModelData model)
	{
		refreshRow(store.indexOf(model));
	}

	protected ModelData prepareData(ModelData model)
	{
		if (grid.getModelProcessor() != null)
		{
			boolean silent = false;
			if (model instanceof BaseModel)
			{
				silent = ((BaseModel) model).isSilent();
				((BaseModel) model).setSilent(true);
			}

			ModelData m = grid.getModelProcessor().prepareData(model);

			if (model instanceof BaseModel)
			{
				((BaseModel) model).setSilent(silent);
			}

			return m;
		}
		return model;
	}

	protected void processRows(int startRow, boolean skipStripe)
	{
		if (ds.getCount() < 1)
		{
			return;
		}
		skipStripe = skipStripe || !grid.isStripeRows();
		NodeList<Element> rows = getRows();
		String cls = "x-grid3-row-alt";
		for (int i = 0, len = rows.getLength(); i < len; i++)
		{
			Element row = rows.getItem(i);
			row.setPropertyInt("rowIndex", i);
			if (!skipStripe)
			{
				boolean isAlt = (i + 1) % 2 == 0;
				boolean hasAlt = row.getClassName() != null
						&& row.getClassName().indexOf(cls) != -1;
				if (isAlt == hasAlt)
				{
					continue;
				}
				if (isAlt)
				{
					El.fly(row).addStyleName(cls);
				} else
				{
					El.fly(row).removeStyleName(cls);
				}
			}
		}
	}

	@SuppressWarnings("unchecked")
	protected void refreshRow(int row)
	{
		if (grid != null && grid.isViewReady())
		{
			ModelData m = ds.getAt(row);
			if (m != null)
			{
				// do not change focus on refresh
				// handles situation with changing cell value with field binding
				focusEnabled = false;

				insertRows(ds, row, row, true);
				getRow(row).setPropertyInt("rowIndex", row);
				onRemove(ds, m, row + 1, true);

				renderWidgets(row, row);

				GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
				e.setRowIndex(row);
				e.setModel(ds.getAt(row));
				fireEvent(Events.RowUpdated, e);
				focusEnabled = true;
			}
		}
	}

	protected void removeRow(int row)
	{
		Element r = getRow(row);
		if (r != null)
		{
			fly(r).removeFromParent();
		}
	}

	protected void removeRowStyle(Element row, String style)
	{
		fly(row).removeStyleName(style);
	}

	protected void render()
	{
		renderUI();
		grid.sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
				| Event.FOCUSEVENTS);
	}

	protected void renderFooter()
	{
		if (!footer.isRendered())
		{
			footer.disableTextSelection(true);
			footer.render(mainWrap.dom);
		} else
		{
			mainWrap.appendChild(footer.getElement());
		}
	}

	protected void renderHeader()
	{
		El head = grid.el().selectNode(".x-grid3-hh");
		head.removeChildren();
		if (!header.isRendered())
		{
			header.render(head.dom);
		} else
		{
			head.appendChild(header.getElement());
		}
	}

	protected String renderRows(int startRow, int endRow)
	{
		int colCount = cm.getColumnCount();

		if (ds.getCount() < 1)
		{
			return "";
		}

		List<ColumnData> cs = getColumnData();

		if (endRow == -1)
		{
			endRow = ds.getCount() - 1;
		}

		List<ModelData> rs = ds.getRange(startRow, endRow);
		return doRender(cs, rs, startRow, colCount, grid.isStripeRows());
	}

	protected void renderUI()
	{
		String h = "<div class='x-grid3-hh' role='row'></div>";
		String body = templates.body("");

		String html = templates.master(body, h);

		grid.getElement().setInnerHTML(html);

		renderHeader();

		initElements();

		header.setEnableColumnResizing(grid.isColumnResize());
		header.setEnableColumnReorder(grid.isColumnReordering());

		if (footer != null)
		{
			renderFooter();
		}

		updateHeaderSortState();
	}

	protected void renderWidgets(int startRow, int endRow)
	{
		if (grid.isViewReady())
		{
			if (endRow == -1)
			{
				endRow = widgetList.size() - 1;
			}
			for (int i = startRow; i <= endRow; i++)
			{
				List<Widget> m = i < widgetList.size() ? widgetList.get(i)
						: null;
				if (m != null)
				{
					for (int j = 0; j < grid.getColumnModel().getColumnCount(); j++)
					{
						Widget w = j < m.size() ? m.get(j) : null;
						if (w != null)
						{
							Element cell = getWidgetCell(i, j);
							if (cell != null)
							{
								if (w.getElement().getParentElement() != cell)
								{
									fly(cell).removeChildren();
									cell.appendChild(w.getElement());
								}
								if (grid.isAttached())
								{
									ComponentHelper.doAttach(w);
								}
							}
						}
					}
				}
			}
		}
	}

	protected void resize()
	{
		if (mainBody == null)
		{
			return;
		}

		El c = grid.el();
		Size csize = c.getStyleSize();

		int vw = csize.width;
		int vh = 0;
		if (vw < 10 || csize.height < 20)
		{
			return;
		}

		if (grid.isAutoHeight())
		{
			el.setWidth(csize.width);
			scroller.setWidth(vw);
		} else
		{
			el.setSize(csize.width, csize.height);
		}

		int hdHeight = innerHd.getHeight();
		vh = csize.height - hdHeight;

		if (footer != null)
		{
			vh -= footer.getHeight();
		}

		if (!grid.isAutoHeight())
		{
			scroller.setSize(vw, vh);
		}

		if (innerHd != null)
		{
			innerHd.setWidth(vw);
		}
		if (footer != null)
		{
			footer.setWidth(vw);
		}

	}

	protected void restoreScroll(Point state)
	{
		if (state.y < scroller.getWidth())
		{
			scroller.setScrollLeft(state.x);
		}
		if (state.x < scroller.getHeight())
		{
			scroller.setScrollTop(state.y);
		}
	}

	protected void stopEditing()
	{
		if (grid.editSupport != null)
		{
			grid.editSupport.stopEditing(true);
		}
	}

	protected void syncHeaderScroll()
	{
		int sl = scroller.getScrollLeft();
		innerHd.setScrollLeft(sl);
		// second time for IE (1/2 time first fails, other browsers ignore)
		innerHd.setScrollLeft(sl);

		if (footer != null)
		{
			footer.el().setScrollLeft(sl);
			footer.el().setScrollLeft(sl);
		}
	}

	protected void syncHScroll()
	{
		if (!hasRows())
		{
			El child = mainBody.firstChild();

			if (child != null)
			{
				child.setWidth(cm.getTotalWidth(), true);
			}
		}
	}

	@SuppressWarnings("unchecked")
	protected void syncScroll()
	{
		syncHeaderScroll();
		int scrollLeft = scroller.getScrollLeft();
		int scrollTop = scroller.getScrollTop();
		GridEvent<ModelData> ge = (GridEvent<ModelData>) createComponentEvent(null);
		ge.setScrollLeft(scrollLeft);
		ge.setScrollTop(scrollTop);
		constrainFocusElement();
		grid.fireEvent(Events.BodyScroll, ge);
	}

	protected void templateAfterMove(int index)
	{
		// template method
	}

	protected void templateOnAllColumnWidthsUpdated(List<Integer> ws, int tw)
	{
		// template method
	}

	protected void templateOnColumnHiddenUpdated(int col, boolean hidden, int tw)
	{
		// template method
	}

	protected void templateOnColumnWidthUpdated(int col, int w, int tw)
	{
		// template method
	}

	protected void templateOnLayout(int vw, int vh)
	{
		// template method
	}

	protected void templateUpdateColumnText(int col, String text)
	{
		// template method
	}

	protected void updateAllColumnWidths()
	{
		int tw = getTotalWidth();
		int clen = cm.getColumnCount();
		Stack<Integer> ws = new Stack<Integer>();

		for (int i = 0; i < clen; i++)
		{
			ws.push(getColumnWidth(i));
			header.updateColumnWidth(i, cm.getColumnWidth(i));
			if (footer != null)
			{
				footer.updateColumnWidth(i, cm.getColumnWidth(i));
			}
		}

		NodeList<Element> ns = getRows();
		for (int i = 0, len = ns.getLength(); i < len; i++)
		{
			Element row = ns.getItem(i);
			row.getStyle().setPropertyPx("width", tw);
			if (row.getFirstChild() != null)
			{
				row.getFirstChildElement().getStyle()
						.setPropertyPx("width", tw);
				TableSectionElement e = row.getFirstChild().cast();
				TableRowElement nodeList = e.getRows().getItem(0);
				for (int j = 0; j < clen; j++)
				{
					((Element) nodeList.getChildNodes().getItem(j)).getStyle()
							.setPropertyPx("width", ws.get(j));
				}
			}
		}

		templateOnAllColumnWidthsUpdated(ws, tw);
		syncHScroll();
	}

	protected void updateColumnHidden(int index, boolean hidden)
	{
		int tw = getTotalWidth();
		String display = hidden ? "none" : "";

		El.fly(innerHd.dom.getFirstChildElement()).setWidth(getOffsetWidth());
		El.fly(innerHd.dom.getFirstChildElement().getFirstChildElement())
				.setWidth(tw);

		header.updateColumnHidden(index, hidden);
		if (footer != null)
		{
			footer.updateTotalWidth(getOffsetWidth(), tw);
			footer.updateColumnHidden(index, hidden);
		}

		NodeList<Element> ns = getRows();
		for (int i = 0, len = ns.getLength(); i < len; i++)
		{
			Element elem = ns.getItem(i);
			elem.getStyle().setProperty("width", tw + "px");
			TableSectionElement e = (TableSectionElement) elem.getFirstChild();
			if (e != null)
			{
				e.getStyle().setProperty("width", tw + "px");
				Element cell = e.getRows().getItem(0).getChildNodes()
						.getItem(index).cast();
				cell.getStyle().setProperty("display", display);
			}
		}

		templateOnColumnHiddenUpdated(index, hidden, tw);

		lastViewWidth = -1;
		layout();
		syncHScroll();
	}

	protected void updateColumnWidth(int col, int width)
	{
		int tw = getTotalWidth();
		int w = getColumnWidth(col);

		header.updateTotalWidth(-1, tw);
		header.updateColumnWidth(col, width);

		if (footer != null)
		{
			footer.updateTotalWidth(getOffsetWidth(), tw);
			footer.updateColumnWidth(col, width);
		}

		NodeList<Element> ns = getRows();
		for (int i = 0, len = ns.getLength(); i < len; i++)
		{
			Element row = ns.getItem(i);
			row.getStyle().setPropertyPx("width", tw);
			if (row.getFirstChild() != null)
			{
				row.getFirstChildElement().getStyle()
						.setPropertyPx("width", tw);
				TableSectionElement e = row.getFirstChild().cast();
				((Element) e.getRows().getItem(0).getChildNodes().getItem(col))
						.getStyle().setPropertyPx("width", w);
			}
		}

		templateOnColumnWidthUpdated(col, w, tw);
		syncHScroll();
	}

	@SuppressWarnings("unchecked")
	protected void updateHeaderSortState()
	{
		SortInfo state = getSortState();
		if (state == null || Util.isEmptyString(state.getSortField()))
		{
			return;
		}
		if (sortState == null
				|| (!sortState.getSortField().equals(state.getSortField()))
				|| sortState.getSortDir() != state.getSortDir())
		{

			GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
			e.setSortInfo(state);

			sortState = new SortInfo(state.getSortField(), state.getSortDir());
			int sortColumn = cm.findColumnIndex(state.getSortField());
			if (sortColumn != -1)
			{
				updateSortIcon(sortColumn, sortState.getSortDir());
			}
			if (grid.isStateful())
			{
				Map<String, Object> st = grid.getState();
				st.put("sortField", sortState.getSortField());
				st.put("sortDir", sortState.getSortDir().toString());
				grid.saveState();
			}
			grid.fireEvent(Events.SortChange, e);
		}
	}

	protected void updateSortIcon(int colIndex, SortDir dir)
	{
		header.updateSortIcon(colIndex, dir);
	}

	private native String getCellIndexId(Element elem) /*-{
		if (!@com.extjs.gxt.ui.client.widget.grid.GridView::colRe) {
			@com.extjs.gxt.ui.client.widget.grid.GridView::colRe = new RegExp(
					"x-grid3-td-([^\\s]+)");
		}
		if (elem) {
			var m = elem.className
					.match(@com.extjs.gxt.ui.client.widget.grid.GridView::colRe);
			if (m && m[1]) {
				return m[1];
			}
		}
		return null;
	}-*/;

	private void refreshFooterData()
	{
		if (footer != null)
		{
			footer.refresh();
		}
	}

	private void restrictMenu(Menu columns)
	{
		int count = 0;
		for (int i = 0, len = cm.getColumnCount(); i < len; i++)
		{
			if (!shouldNotCount(i, true))
			{
				count++;
			}
		}

		if (count == 1)
		{
			for (Component item : columns.getItems())
			{
				CheckMenuItem ci = (CheckMenuItem) item;
				if (ci.isChecked())
				{
					ci.disable();
				}
			}
		} else
		{
			for (Component item : columns.getItems())
			{
				item.enable();
			}
		}
	}

	private boolean shouldNotCount(int columnIndex, boolean includeHidden)
	{
		return cm.getColumnHeader(columnIndex) == null
				|| cm.getColumnHeader(columnIndex).equals("")
				|| (includeHidden && cm.isHidden(columnIndex))
				|| cm.isFixed(columnIndex);
	}

	private void constrainFocusElement()
	{
		int scrollLeft = scroller.dom.getScrollLeft();
		int scrollTop = scroller.dom.getScrollTop();
		int left = scroller.getWidth(true) / 2 + scrollLeft;
		int top = scroller.getHeight(true) / 2 + scrollTop;
		focusEl.setLeftTop(left, top);
	}

}
